import copy
from typing import List

from .gotypes import Player, Point
from .zobrist import EmptyCode, HashCode
from .score import ComputeGameResult
import numpy as np

class Move:
    """
    通过类方法 Play(), Pass(), Resign() 来构造 Move 对象.
    """

    def __init__(
        self, point: Point = None, is_pass: bool = False, is_resign: bool = False
    ):
        assert (point is not None) ^ is_pass ^ is_resign
        self.point: Point = point
        self.is_play: bool = (self.point is not None)
        self.is_pass: bool = is_pass
        self.is_resign: bool = is_resign

    @classmethod
    def Play(cls, point: Point) -> "Move":
        return Move(point=point)

    @classmethod
    def Pass(cls) -> "Move":
        return Move(is_pass=True)

    @classmethod
    def Resign(cls) -> "Move":
        return Move(is_resign=True)

    def __str__(self) -> str:
        if self.is_pass:
            return "pass"
        if self.is_resign:
            return "resign"
        return f"(row={self.point.row}, col={self.point.col})"

    def __hash__(self) -> int:
        return hash((self.is_play, self.is_pass, self.is_resign, self.point))
    
    def __eq__(self, other: "Move") -> bool:
        return (self.is_pass == other.is_pass and self.is_play == self.is_play 
                and self.is_resign == self.is_resign and self.point == self.point)


class GoString:
    def __init__(self, next_player: Player, stones: set, liberties: set):
        self.next_player: Player = next_player
        self.stones: frozenset[Point] = frozenset(stones)
        self.liberties: frozenset[Point] = frozenset(liberties)

    def WithoutLiberity(self, point: Point) -> "GoString":
        new_liberties = self.liberties - set([point])
        return GoString(self.next_player, self.stones, new_liberties)

    def WithLiberity(self, point: Point) -> "GoString":
        new_liberties = self.liberties | set([point])
        return GoString(self.next_player, self.stones, new_liberties)

    def MergedWith(self, go_string: "GoString") -> "GoString":
        assert go_string.next_player == self.next_player
        combined_stones = self.stones | go_string.stones
        return GoString(
            self.next_player,
            combined_stones,
            (self.liberties | go_string.liberties) - combined_stones,
        )

    @property
    def NumberLiberties(self) -> int:
        return len(self.liberties)

    def __eq__(self, other: "GoString") -> bool:
        return (
            isinstance(other, GoString)
            and self.next_player == other.next_player
            and self.stones == other.stones
            and self.liberties == other.liberties
        )

    def __deepcopy__(self, momodict={}):
        return GoString(self.next_player, self.stones, copy.deepcopy(self.liberties))

class Board:
    """
    放置和捕获棋子
    """

    def __init__(self, rows: int, cols: int):
        self.rows = rows
        self.cols = cols
        self.grid: dict[Point, GoString] = {}
        self.board_code = EmptyCode

    def PlaceStone(self, player: Player, point: Point):
        assert self.IsOnGrid(point)
        assert self.grid.get(point) is None
        adjacent_same_string: list[GoString] = []
        adjacent_opposite_string: list[GoString] = []
        liberties: list[Point] = []

        for neighbor in point.Neighbors():
            if not self.IsOnGrid(neighbor):
                continue
            neighbor_string: GoString = self.grid.get(neighbor)
            if neighbor_string is None:
                liberties.append(neighbor)
            elif neighbor_string.next_player == player:
                if neighbor_string not in adjacent_same_string:
                    adjacent_same_string.append(neighbor_string)
            else:
                if neighbor_string not in adjacent_opposite_string:
                    adjacent_opposite_string.append(neighbor_string)
        new_string = GoString(player, [point], liberties)
        for same in adjacent_same_string:
            new_string = new_string.MergedWith(same)
        for new_string_point in new_string.stones:
            self.grid[new_string_point] = new_string

        self.board_code ^= HashCode[point, player]

        for other_string in adjacent_opposite_string:
            replacement: GoString = other_string.WithoutLiberity(point)
            if replacement.NumberLiberties:
                self.ReplaceString(other_string.WithoutLiberity(point))
            else:
                self.RemoveString(other_string)

    def ReplaceString(self, new_string: GoString):
        for point in new_string.stones:
            self.grid[point] = new_string

    def IsOnGrid(self, point: Point):
        return 1 <= point.row <= self.rows and 1 <= point.col <= self.cols

    def GetPlayer(self, point: Point):
        gostring = self.grid.get(point)
        if gostring is None:
            return None
        return gostring.next_player

    def GetGoString(self, point: Point):
        gostring: GoString = self.grid.get(point)
        if gostring is None:
            return None
        return gostring

    def RemoveString(self, gostring: GoString):
        for point in gostring.stones:
            for neighbor in point.Neighbors():
                neighbor_string = self.grid.get(neighbor)
                if neighbor_string is None:
                    continue
                if neighbor_string is not gostring:
                    self.ReplaceString(neighbor_string.WithLiberity(point))
            self.grid[point] = None
            self.board_code ^= HashCode[point, gostring.next_player]

    @property
    def BoardCode(self):
        return self.board_code

    def __eq__(self, other: "Board") -> bool:
        return (
            isinstance(other, Board)
            and self.rows == other.rows
            and self.cols == other.cols
            and self.board_code == other.board_code
        )

    def __deepcopy__(self, memodict={}):
        copied = Board(self.rows, self.cols)
        copied.grid = copy.copy(self.grid)
        copied.board_code = self.board_code
        return copied


class GameState:
    def __init__(
        self,
        board: Board,
        next_player: Player,
        previous_state: "GameState",
        last_move: Move,
    ):
        self.board = board
        self.next_player = next_player
        self.previous_state = previous_state
        if previous_state is None:
            self.previous_states = frozenset()
        else:
            self.previous_states = frozenset(
                previous_state.previous_states
                | {(previous_state.next_player, previous_state.board.BoardCode)}
            )
        self.last_move = last_move

    def ApplyMove(self, move: Move):
        if move.is_play:
            next_board = copy.deepcopy(self.board)
            next_board.PlaceStone(self.next_player, move.point)
        else:
            next_board = self.board
        return GameState(next_board, self.next_player.Other, self, move)

    @classmethod
    def NewGame(self, board_size: int):
        if isinstance(board_size, int):
            board_size = (board_size, board_size)
        board = Board(*board_size)
        return GameState(board, Player.black, None, None)

    def IsOver(self):
        if self.last_move is None:
            return False
        if self.last_move.is_resign:
            return True
        second_last_move = self.previous_state.last_move
        if second_last_move is None:
            return False
        return self.last_move.is_pass and second_last_move.is_pass

    def IsMoveSelfCapture(self, player: Player, move: Move):
        if not move.is_play:
            return False
        next_board = copy.deepcopy(self.board)
        next_board.PlaceStone(player, move.point)
        new_string = next_board.GetGoString(move.point)
        return new_string.NumberLiberties == 0

    @property
    def Situation(self):
        return (self.next_player, self.board.BoardCode)

    def DoesMoveViolateKo(self, player: Player, move: Move):
        if not move.is_play:
            return False
        next_board: Board = copy.deepcopy(self.board)
        next_board.PlaceStone(player, move.point)
        next_situation = (player.Other, next_board.BoardCode)
        return next_situation in self.previous_states

    def IsValidMove(self, move: Move):
        if self.IsOver():
            return False
        if move.is_pass or move.is_resign:
            return True
        return (
            self.board.GetPlayer(move.point) is None
            and not self.IsMoveSelfCapture(self.next_player, move)
            and not self.DoesMoveViolateKo(self.next_player, move)
        )

    def LegalMoves(self) -> List[Move]:
        moves: List[Move] = []
        for row in range(1, self.board.rows + 1):
            for col in range(1, self.board.cols + 1):
                move = Move.Play(Point(row, col))
                if self.IsValidMove(move):
                    moves.append(move)
        moves.append(Move.Pass())
        moves.append(Move.Resign())
        return moves

    def Winner(self):
        if not self.IsOver():
            return None
        if self.last_move.is_resign:
            return self.next_player
        result = ComputeGameResult(self)
        return result.Winner


def IsPointAnEye(board: Board, point: Point, player: Player):
    if board.GetPlayer(point) is not None:
        return False
    for neighbor in point.Neighbors():
        if board.IsOnGrid(neighbor):
            neighbor_player = board.GetPlayer(neighbor)
            if neighbor_player != player:
                return False
    friendly_corners = 0
    off_board_corners = 0
    corners = [
        Point(point.row - 1, point.col - 1),
        Point(point.row - 1, point.col + 1),
        Point(point.row + 1, point.col - 1),
        Point(point.row + 1, point.col + 1),
    ]
    for corner in corners:
        if board.IsOnGrid(corner):
            corner_player = board.GetPlayer(corner)
            if corner_player == player:
                friendly_corners += 1
        else:
            off_board_corners += 1
    if off_board_corners > 0:
        return off_board_corners + friendly_corners == 4
    return friendly_corners >= 3
